/* 
 * StikiWeb Wiki - a Wiki for Hierarchical Organizations
 * $Id: AttachmentManager.java,v 1.9 2006/07/20 21:07:02 biglee Exp $
 * 
 * ***** BEGIN LICENSE BLOCK *****
 * Version: MPL 1.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is the StikiWeb Wiki 
 *
 * The Initial Developer of the Original Code is Lee Haslup (biglee@haslups.com).
 * Portions created by the Initial Developer are Copyright (C) 2004
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *
 * ***** END LICENSE BLOCK ***** 
 */
 package org.stikiweb.contentMagnager;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.util.HashMap;
import java.util.zip.ZipEntry;
import java.util.zip.ZipException;
import java.util.zip.ZipFile;

import org.apache.struts.upload.FormFile;
import org.stikiweb.translations.StikiWebTranslator;

/**
 * @author Allen L (BigLee) Haslup
 *
 * 
 */
public class AttachmentManager {
	
	// no-argument constructor is private.  Only way to instantiate is through
	// solo() method that returns singleton instance.
	private AttachmentManager(){}
	private static AttachmentManager solo;
	public static AttachmentManager solo() {
		if(solo==null) {
			solo=new AttachmentManager();
		}
		return solo;
	}

	// this table of mime types and their associated extensions is used to build a 
	// HashMap to look up mime types.  The odd numbered entried are the extensions
	// and the immediately preceeding even entries (starting w/ zero) are the mime types.
	static final String[] rawMimeData= {
		"application/mac-binhex40",	"hqx",
		"application/mac-compactpro",	"cpt",
		"application/msword",	"doc",
		"application/octet-stream",	"bin",
		"application/octet-stream",	"dms",
		"application/octet-stream",	"lha",
		"application/octet-stream",	"lzh",
		"application/octet-stream",	"exe",
		"application/octet-stream",	"class",
		"application/oda",	"oda",
		"application/pdf",	"pdf",
		"application/postscript",	"ai",
		"application/postscript",	"eps",
		"application/postscript",	"ps",
		"application/rtf",	"rtf",
		"application/smil",	"smi",
		"application/smil",	"smil",
		"application/vnd.mif",	"mif",
		"application/vnd.ms-excel",	"xls",
		"application/vnd.ms-powerpoint",	"ppt",
		"application/x-bcpio",	"bcpio",
		"application/x-cdlink",	"vcd",
		"application/x-chess-pgn",	"pgn",
		"application/x-cpio",	"cpio",
		"application/x-csh",	"csh",
		"application/x-director",	"dcr",
		"application/x-director",	"dir",
		"application/x-director",	"dxr",
		"application/x-dvi",	"dvi",
		"application/x-futuresplash",	"spl",
		"application/x-gtar",	"gtar",
		"application/x-hdf",	"hdf",
		"application/x-javascript",	"js",
		"application/x-koan",	"skp",
		"application/x-koan",	"skd",
		"application/x-koan",	"skt",
		"application/x-koan",	"skm",
		"application/x-latex",	"latex",
		"application/x-netcdf",	"nc",
		"application/x-netcdf",	"cdf",
		"application/x-sh",	"sh",
		"application/x-shar",	"shar",
		"application/x-shockwave-flash",	"swf",
		"application/x-stuffit",	"sit",
		"application/x-sv4cpio",	"sv4cpio",
		"application/x-sv4crc",	"sv4crc",
		"application/x-tar",	"tar",
		"application/x-tcl",	"tcl",
		"application/x-tex",	"tex",
		"application/x-texinfo",	"texinfo",
		"application/x-texinfo",	"texi",
		"application/x-troff",	"t",
		"application/x-troff",	"tr",
		"application/x-troff",	"roff",
		"application/x-troff-man",	"man",
		"application/x-troff-me",	"me",
		"application/x-troff-ms",	"ms",
		"application/x-ustar",	"ustar",
		"application/x-wais-source",	"src",
		"application/zip",	"zip",
		"audio/basic",	"au",
		"audio/basic",	"snd",
		"audio/midi",	"mid",
		"audio/midi",	"midi",
		"audio/midi",	"kar",
		"audio/mpeg",	"mpga",
		"audio/mpeg",	"mp2",
		"audio/mpeg",	"mp3",
		"audio/x-aiff",	"aif",
		"audio/x-aiff",	"aiff",
		"audio/x-aiff",	"aifc",
		"audio/x-pn-realaudio",	"ram",
		"audio/x-pn-realaudio",	"rm",
		"audio/x-pn-realaudio-plugin",	"rpm",
		"audio/x-realaudio",	"ra",
		"audio/x-wav",	"wav",
		"chemical/x-pdb",	"pdb",
		"chemical/x-pdb",	"xyz",
		"image/bmp",	"bmp",
		"image/gif",	"gif",
		"image/ief",	"ief",
		"image/jpeg",	"jpeg",
		"image/jpeg",	"jpg",
		"image/jpeg",	"jpe",
		"image/png",	"png",
		"image/tiff",	"tiff",
		"image/tiff",	"tif",
		"image/x-cmu-raster",	"ras",
		"image/x-portable-anymap",	"pnm",
		"image/x-portable-bitmap",	"pbm",
		"image/x-portable-graymap",	"pgm",
		"image/x-portable-pixmap",	"ppm",
		"image/x-rgb",	"rgb",
		"image/x-xbitmap",	"xbm",
		"image/x-xpixmap",	"xpm",
		"image/x-xwindowdump",	"xwd",
		"model/iges",	"igs",
		"model/iges",	"iges",
		"model/mesh",	"msh",
		"model/mesh",	"mesh",
		"model/mesh",	"silo",
		"model/vrml",	"wrl",
		"model/vrml",	"vrml",
		"text/css",	"css",
		"text/html",	"html",
		"text/html",	"htm",
		"text/plain",	"asc",
		"text/plain",	"txt",
		"text/richtext",	"rtx",
		"text/rtf",	"rtf",
		"text/sgml",	"sgml",
		"text/sgml",	"sgm",
		"text/tab-separated-values",	"tsv",
		"text/x-setext",	"etx",
		"text/xml",	"xml",
		"video/mpeg",	"mpeg",
		"video/mpeg",	"mpg",
		"video/mpeg",	"mpe",
		"video/quicktime",	"qt",
		"video/quicktime",	"mov",
		"video/x-msvideo",	"avi",
		"video/x-sgi-movie",	"movie",
		"x-conference/x-cooltalk",	"ice"
	};
	
	// the mime man is initialized the first time it is needed.
	private HashMap mimeMap = null;	
	
	public synchronized String mimeFromExt(String ext) {
		
		if(mimeMap==null) {
			mimeMap = new HashMap();
			int len = rawMimeData.length;
			for(int i=1; i<len; i+=2) {
				mimeMap.put(rawMimeData[i],rawMimeData[i-1]);
			}
		}
		//consult the map for the mime type 
		//Note: expensions are case-insensitive
		String retval=(String)mimeMap.get(ext.toLowerCase());
		return (retval==null)?"application/octet-stream":retval;
	}
	
	// TODO: very similar to PageManager method.  Refactor?
	public boolean attachmentExists(String name ) {
		String realPath = StikiWebContext.solo().wikiBase;
		String fileName = StikiWebTranslator.convertName(name);
		// TODO: cache?
		int zipLoc = fileName.indexOf(".zip/");
		if(zipLoc>=0) {
			fileName = fileName.substring(0,zipLoc+4);
		}
		File file = new File(realPath, fileName);
		return file.exists();
	}

	public InputStream getAttachmentStream(String name)
		throws PageManagerException {
		String realPath = StikiWebContext.solo().wikiBase;
		String fileName = StikiWebTranslator.convertName(name);
		// TODO: cache?
		
		// If the file we are looking for is inside a zip file
		// ie. filename is like-- mydir/mypage/myzip.zip/theFileIWant.txt
		int zipLoc = fileName.indexOf(".zip/");
		if(zipLoc>=0) {
			return zipStream(name,realPath,fileName,zipLoc);
		}
		
		// just a regular attachment.
		File attFile = new File(realPath + fileName);
		
		if(!attFile.exists() || !attFile.isFile()) {
			throw new PageManagerException("Attachment file " + name + " not found.");
		}
		if(!attFile.canRead()) {
			throw new PageManagerException("Attachment file " + name + " not readable.");
		}

		// see if we can open the file.
		InputStream in;
		try {
			in = new FileInputStream(attFile);
		} catch (FileNotFoundException e1) {
			// should have caught this one above.
			return null;
		}
		return in;
	}

 	/**
	 * @param fileName
	 * @param zipLoc
	 * @return
 	 * @throws PageManagerException
	 */
	private InputStream zipStream(String name, String realPath, String fileName, int zipLoc) throws PageManagerException {
		
		// if fileName is a/b.zip/c then zipLoc will be index of the dot.
		// compute zipFileName = a/b.zip  and   zipContentFileName = c 
		String zipFileName = fileName.substring(0,zipLoc+4);
		String zipContentFileName = fileName.substring(zipLoc+5);
		
		File attFile = new File(realPath + zipFileName);
		
		if(!attFile.exists() || !attFile.isFile()) {
			throw new PageManagerException("Attachment file " + name + " not found.");
		}
		if(!attFile.canRead()) {
			throw new PageManagerException("Attachment file " + name + " not readable.");
		}
		
		//we've found the zip file and we can read it.  Open it as a ZipFile
		ZipFile z = null;
		try {
			z = new ZipFile(attFile);
		} catch (ZipException e) {
			throw new PageManagerException("Cannot open zip file " + name + ".");
		} catch (IOException e) {
			throw new PageManagerException("Cannot open zip file " + name + ".");
		}
		
		// if we are looking for a particular file
		if(zipContentFileName.length()>0) {
			ZipEntry ze = z.getEntry(zipContentFileName);
			if(ze==null) {
				throw new PageManagerException("Zip file does not contain" + zipContentFileName + ".");
			}
			try {
				return z.getInputStream(ze);
			} catch (IOException e1) {
				throw new PageManagerException("Zip file I/O exception:" + e1.toString() + ".");
			}
		}
		throw new PageManagerException("Zip file error...");
	}

	public String getAttachment(String name)
 		throws PageManagerException {
 		InputStream in = getAttachmentStream(name);
		if (in == null) {
			throw new PageManagerException("File " + name + " not found.");
		}
		//read it in
		StringWriter sw = new StringWriter();
		try {
			int c = in.read();
			while (c >= 0) {
				sw.write(c);
				c = in.read();
			}
		} catch (IOException e) {
			e.printStackTrace();
			throw new PageManagerException(
				"I/O error reading file " + name + ".");
		}
		// TODO: cache?
		return sw.toString(); 		
 	}
 	
 	public void attach(String pageName, FormFile file)
 		throws PageManagerException 
 	{
 		// TODO: Most of this method is the same as PageManager.savePage -- refactor.
 		String attName = file.getFileName();
 		String fileName = StikiWebTranslator.convertName(pageName+"/"+attName);
		String path = StikiWebContext.solo().wikiBase;
		File outFile = new File(path,fileName);
		if(outFile.exists()) {
			// file exists, make sure we can write it.
			if(!outFile.canWrite()) {
				throw new PageManagerException("File "+fileName+" not writable.");
			} 
		}
		// file does not exist, make sure we can create it
		else {
			File parent = outFile.getParentFile();
			if(!parent.exists()) {
				if(!parent.mkdirs()) {
					throw new PageManagerException("Cannot create directory "+ parent + ".");
				}
			}
			try {
				if(!outFile.createNewFile()) {
					throw new PageManagerException("Cannot Create file" + outFile + ".");
				}
			} catch (IOException e1) {
				e1.printStackTrace();
				throw new PageManagerException("Cannot Create file" + outFile + " - I/O error.");
			}
		}
		// file is good to go at this point.
		try {
			FileOutputStream fo = new FileOutputStream(outFile);
			InputStream fi = file.getInputStream();
			int c = fi.read();
			while(c>=0) {
				fo.write(c);
				c=fi.read();
			}
			fo.flush();
			fo.close();
			
			// Update the PageInfoNode tree
			PageInfoNode theNode = PageInfoNode.forName(pageName);
			theNode.addAttachment(attName);
		}
		catch (IOException e) {
			throw new PageManagerException("IO Error Attaching File.");
		}
 	}
 	public void deleteAttachment(String name) 
		throws PageManagerException {
		String fileName = StikiWebTranslator.convertName(name);
		String path = StikiWebContext.solo().wikiBase;
	
		// the file we are trying to delete.
		File obsoleteFile = new File(path,fileName);
		
		// should exist and be an ordinary file
		if(!obsoleteFile.exists() || !obsoleteFile.isFile()) {
			// file doesn't exist
			throw new PageManagerException("File "+name+" not found.");
		}
		
		// try to delete the file.
		if(obsoleteFile.delete()) {
			
			// file is gone.  Tidy up a bit.  Determine name of page to which file was attached
			// and attachment file name (w/o path info).
			String pageName = name.substring(0,name.indexOf('/'));
			String attName = name.substring(name.indexOf('/')+1);
			
			// then from the Page Info Node data structure.
			PageInfoNode node = PageInfoNode.forName(pageName);
			node.dropAttachment(attName);
			
		}
		else {
			throw new PageManagerException("I/O Error Deleting "+ fileName);
		}
			
 		
 	}

	public static void reset() {
		// If the singleton instance is present, cut it loose
		// so the garbage collector can get it.
		if(solo!=null){
			solo.mimeMap = null;
			solo=null;
		}
	}
}
